跳到主要内容

第四章 前后台程序与状态机

通过掌握STM32的GPIO输入输出功能,能够实现LED的开关控制和按键检测。其中,可靠的按键检测除了需要读取GPIO电平外,还必须进行消抖处理(通常借助定时器实现);而当检测逻辑需要处理多个状态时,基于状态机的方法则更为实用和高校,本章将重点介绍这一方法。

4.1STM32的定时器前后台程序

定时器对于已经有51单片机基础的读者并不陌生,这是一个单片机最常用的内部外设之一。标准51单片机只有Timer0和Timer1两个16位的定时器,并且只能向上溢出。而Kingst32开发板所采用的STM32F103VCT6具备有8个定时器,可以向上溢出,向下溢出以及中央对齐等模式。除此之外,还可以设置自动装载初值,不需要用户在中断程序重新赋值,还有专门的分频器等各种更加强大的功能。 配置定时器,通过周期性中断的方式运行定时中断程序,称之为前台程序(类似于急诊医生),其余代码放在主程序中运行,构成了定时器后台程序(类似于门诊医生)。 创建定时器6,定时时间20ms,在中断服务函数中,调用按键扫描的代码,在主程序运行LED流水灯的程序。 双击CubeMX工程,在Pinout&Configuration页面下,左侧列表Timer中找到基本定时器TIM6,勾选Activated激活,如图4-1所示。

图4-1 选择定时器TIM6

TIM6是STM32中的一个基本定时器,从Clock Configuration可以看出,时钟来源是APB1的倍频,频率是72M,如图4-2所示。

图4-2 定时器TIM6时钟源

学习51单片机的时候,定时器的定时周期从0到71ms左右,如果想定时时间更长则需要采用倍数关系处理。而STM32内部集成了Perscaler(预分频器)用来降低时钟频率的硬件模块,通过Perscaler和Counter_Period(计数周期)来计算定时周期。 按照(Perscaler+1)*(Counter_Period)/72000000=20ms进行计算,即要实现(Perscaler+1)Counter_Period =20ms72000000=1440000。可以设置Perscaler =7199 ,Counter_Period = 200,使能自动重装载auto-reload preload = Enable,如图4-3所示。在NVIC Settings 勾选全局的中断使能,点击生成MDK代码,如图4-4所示。

图4-3 TIM6定时器配置20ms

图4-4 TIM6定时器中断使能

在key.c文件中,增加以下回调函数。


extern TIM_HandleTypeDef htim6;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM6)//判断定时器
{
keyAction(keyScan());//对键盘进行扫描
}
}

何为回调函数?回调函数本质上是一种“你准备好后叫我”的编程模式。普通函数是调用时立即执行,而回调函数具备“被动”特性--不是立即执行,而是由系统在特定条件下触发时调用。51单片机中的“interrupt”函数和回调函数类似,但本质还不是一个回调函数,具体原因不必深究。

STM32的定时器中断已经被HAL接管,定时器设定的时间达到触发中断后,HAL库就会主动调取这个函数并传参中断来源来执行。由于所有的定时器都调用这个函数,所以程序需要用户根据传递的参数来确定是哪个定时器触发的中断,回调函数运行机制如图4-5所示。

图4-5 定时器回调函数运行机制

TIM_HandleTypeDef是STM32的HAL库中定时器的核心控制结构体,他封装了定时器的所有配置和运行时状态,是HAL库操作定时器的统一接口。主程序中,TIM_HandleTypeDef htim6就是对TIM6的管理。比如在初始化程序HAL_TIM_Base_Init ()时,需要将htim6的地址传入,这部分代码由CubeMX代用户完成;HAL_TIM_Base_Start_IT对定时器启动时,也需要将htim6的地址传入,如下代码所示。

TIM_HandleTypeDef htim6;//HALL提供的对定时器管理的数据句柄

int mian()
{
/*省略其他*/
MX_TIM6_Init();//cubeMX生成的初始化函数
HAL_TIM_Base_Start_IT(&htim6);//启动TIM6
while(1)
{

}
}
static void MX_TIM6_Init(void)//CubeMX生成的定时器初始化代码
{
/*****省略其他***/
}

中断服务函数和主函数构成了前后台程序框架,主程序循环运行,中断周期性打断主程序并执行中断服务程序。这种方法一定程度上提高了程序的可读性,也有一定的扩展性,比如将相同周期的事件任务放在一个定时器的中断服务函数中执行。但是,这种方法要时刻注意定时器中断服务程序的时长,一旦中断服务中花费的事件太长,将会导致前台程序得不到运行。

4.2状态机实现按键检测

在《手把手教你学51单片机——C语言版》中,按键检测采用了基于多次采样与确认的消抖机制,并介绍了长按与短按的基本识别方法。然而,在实际应用场景中,按键功能往往更为复杂,可能需要识别单击、双击、长按乃至组合按键等多种操作。若仅依赖简单的延时消抖与状态判断,已难以满足这类复杂交互的需求。为此,可引入状态机的设计思想,首先构建一个用于按键消抖检测的简单状态机,为后续实现多功能按键识别奠定基础。

此次消抖采用“延时”消抖的办法,即当检测到按键按下后,通过定时器定时20ms后,再次检测按键,如果此刻按键还是按下状态,则认为按键发生了按下的动作。定时器的回调函数20ms为周期,调用按键的扫描程序,当检测到按键弹起动作发生,执行按键事件程序。

首先定义一个枚举体,定义了按键一共有三个状态:空闲状态;消抖状态和按下状态。假设一种场景,按键按下之前,按键检测系统处于空闲状态(即弹起);此刻,按键产生了按下动作,第一个20ms中断检测到按键按下了,按键检测系统切换为消抖状态;第二个20ms中断检测到按键还是低电平,说明按键被按下,按键检测系统依然处于按下状态;如果第二个20ms中断检测到按键是弹起状态,那说明刚才是抖动或者干扰,按键检测系统重新回归空闲状态。后续反复每经过20ms检测一次,直到检测到按键高电平,则认为发生了弹起动作,按键检测系统切换为空闲状态,同时触发按键的单击事件(此处用单击替代按键弹起事件是为了后续按键双击、长按等多事件做准备)。整个按键的状态流程图如图4-6所示。

图4-6 按键状态机流程图(一)

下面编写一段代码,实现的功能:单击按键7和8可以分别点亮一个LED小灯,单击按键9可以熄灭所有的小灯。

  1. 使用枚举体对按键检测过程状态机做一个组合;(类似于51单片机在中断里的keybuf的读取过程,未确认按键事件触发执行动作)
  2. 使用枚举体对按键的事件类型做一个组合;(类似于51单片机的KeySta,确认按键状态后,并且会执行相应按键事件执行动作)
  3. 使用结构体对按键所有相关信息进行封装,包含了状态、事件时间戳、按下标志等所有相关信息,结构体命名为KeyHandle。

Handle(句柄)是嵌入式开发中比较抽象的一个概念,此处要重点介绍。首先要区分handler和handle的区别,handler前边课程有所介绍,是“处理函数”或者“处理程序”的意思。handle和它虽然一个字母之差,但是在嵌入式程序中表达了完全不同的概念。

假设这样一个场景,有张三这样一个人,上班路上9点左右经过海边,发现有人溺水了,然后他立马停车下车救人,而后救完人就上班去了。而同样有李四这样一个人,他没有直接下水救人,但是协助张三做了辅助工作。到了晚上,民政部门通过监控寻找张三和李四,获取他们两个的身份证号,把他们两个每个人的参与的过程做了各自记录,并且计划为他们见义勇为的事件做表彰工作,给张三奖励10万元,李四奖励5万元。

在民政部门的这个表格里,只需要输入张三的身份证号,和张三相关的所有的信息和对张三后续的表彰、奖励金额等事件也都可以找到。比如张三的身份证号、家庭住址、通过监控系统获取的张三今天出门时间、张三的停车时间、张三的救人过程、张三的上班工作岗位,以及哪天开表彰会,给张三奖励的10万元奖金事件... ...同样输入了李四的身份证号,在表格中会出现李四相关的资源和事件。

在这个事件中,如果一共有10个人都参与了救人事件,每个人信息不同,参与救人工作贡献不同,奖金也不同。现在就构建一个结构体,将信息封装在结构体里,可以通过身份证号查找到任何一个人的信息,这个身份证号就可以理解成Handle,在中文文档里称之为句柄。

在这个结构体中,按键当前的状态,按键按下的时间戳,按键触发事件以及按键按下标志,对每一个按键来说都有对应不同的信息。一旦确定了按键(句柄),就确定了这个按键所有的相关内容。创建一个结构体数组,来表达16个按键的所有信息KeyHandle Keys[16]

keyScan函数对矩阵按键进行扫描,调用Key_Scan_Single函数,对单个按键状态之间的条件判断和状态切换,以及对引脚的电平高低判断。

keyAction()是扫描完成后,对事件的响应执行。keyScan()keyAction()在定时器周期回调函数中,20ms周期进行调用,如下所示。

#include "Key.h"

#define TIM_Period 20 // 按键扫描周期ms
#define DEBOUNCE_TIME 20/TIM_Period // 消抖

extern TIM_HandleTypeDef htim6;

typedef enum {
KEY_IDLE, //空闲状态
KEY_STATE_PRESS_DEBOUNCE,//按下消抖状态
KEY_PRESSED,//按下状态
} KeyState;//枚举按键的状态

// 按键事件类型
typedef enum {
KEY_EVENT_NONE,
KEY_EVENT_CLICK, // 单击
} KeyEventType;//枚举按键的事件类型

typedef struct {
KeyState state; // 枚举变量存储按键状态
uint32_t press_timestamp; // 按下时间戳
KeyEventType event; // 枚举变量存储触发事件
uint8_t is_pressed; // 按下标志
} KeyHandle;

KeyHandle Keys[16];//按键状态存储数组

/**按键状态机***/

void Key_Scan_Single(uint8_t KeyNum)
{
uint8_t j =KeyNum%4;

switch (Keys[KeyNum].state){

case KEY_IDLE://空闲状态
Keys[KeyNum].press_timestamp=0;
Keys[KeyNum].is_pressed=0;//按下状态为 0
if(HAL_GPIO_ReadPin(KeyW1_GPIO_Port,KeyW1_Pin<<j)==GPIO_PIN_RESET){//低电平
Keys[KeyNum].state=KEY_STATE_PRESS_DEBOUNCE; //状态跳转到消抖
}
break;

case KEY_STATE_PRESS_DEBOUNCE://按下消抖状态
Keys[KeyNum].press_timestamp+=1;
if(HAL_GPIO_ReadPin(KeyW1_GPIO_Port,KeyW1_Pin<<j)==GPIO_PIN_SET){
Keys[KeyNum].state=KEY_IDLE; //认为是抖动
}
else if(Keys[KeyNum].press_timestamp>=DEBOUNCE_TIME ){
Keys[KeyNum].press_timestamp=0;
Keys[KeyNum].state=KEY_PRESSED; //状态跳转到按下
}

break;

case KEY_PRESSED://按下状态
Keys[KeyNum].is_pressed=1;//按下状态为 1
if(HAL_GPIO_ReadPin(KeyW1_GPIO_Port,KeyW1_Pin<<j)==GPIO_PIN_RESET){//低电平
Keys[KeyNum]. is_pressed=0;
Keys[KeyNum].press_timestamp=0;
}else{
Keys[KeyNum].state=KEY_IDLE;
Keys[KeyNum].event =KEY_EVENT_CLICK;//单击触发条件成立//触发单击事件
}
break;
}
}

/**按键扫描程序***/
void keyScan(void)
{
uint8_t KeyNum;
uint8_t i,j;
for(i=0;i<4;i++){
HAL_GPIO_WritePin(KeyC1_GPIO_Port, KeyC1_Pin<<i, GPIO_PIN_RESET);
__NOP();
__NOP();
__NOP();//延时等待引脚电平稳定
for(j=0;j<4;j++){
KeyNum =j+4*i;//
Key_Scan_Single(KeyNum);//按键状态更新
}
HAL_GPIO_WritePin(KeyC1_GPIO_Port, KeyC1_Pin<<i, GPIO_PIN_SET);
}
}

/***按键动作程序***/
void keyAction()
{
switch (Keys[KET_7].event) {//按键7
case KEY_EVENT_NONE://无事件
break;
case KEY_EVENT_CLICK://单击事件
HAL_GPIO_WritePin(GPIOD,LED1_Pin,GPIO_PIN_RESET);
Keys[KET_7].event = KEY_EVENT_NONE;
break;
}
switch (Keys[KET_8].event) {//按键8
case KEY_EVENT_NONE:
break;
case KEY_EVENT_CLICK://单击事件
HAL_GPIO_WritePin(GPIOD,LED2_Pin,GPIO_PIN_RESET);
Keys[KET_8].event = KEY_EVENT_NONE;
break;

}
switch (Keys[KET_9].event) {//按键9
case KEY_EVENT_NONE:
break;
case KEY_EVENT_CLICK://单击事件
HAL_GPIO_WritePin(GPIOD,LED1_Pin,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD,LED2_Pin,GPIO_PIN_SET);
Keys[KET_9].event = KEY_EVENT_NONE;
break;
}
/**其余按键省略**/
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM6)
{
keyScan();
keyAction();
}
}

以前边这个简单的状态机为原型,扩充状态与条件判断,实现按键单机、双击、长按等多种状态机,实现更加强大的功能,如图4-7所示。

图4-7 按键状态机流程图(二)

如图4-7所示,按键的不同状态可以被描述成一个有限的状态机,通过在不同的状态之间切换,完成不同的条件判断和流程的执行。这种方式的好处非常的多,最明显的一点是程序以非阻塞的方式进行运行。状态之间切换的延时不再调用延时函数,而是使用一个延时变量进行存储,这样就能够非常节省系统的资源的方式进行,相关代码如下所示。

Key.h代码:


#ifndef __KEY_H
#define __KEY_H
#include "main.h"
typedef enum KEY{
KEY_NONE = 0,
KET_7=1,
KET_8,
KET_9,
KET_0,
KET_4,
KET_5,
KET_6,
KET_Down,
KET_1,
KET_2,
KET_3,
KET_Left,
KET_Ok,
KET_Right,
KET_Up,
}Key_Enum;

typedef enum {
KEY_IDLE, //空闲状态
KEY_STATE_PRESS_DEBOUNCE,//按下消抖状态
KEY_PRESSED,//按下状态
KEY_DOUBLE_CHECK,//双击检测状态
KEY_DOUBLE_PRESS_DEBOUNCE,//双击消抖
KEY_DOUBLE_PRESS,//双击确认
KEY_LONG_PRESS,//长按状态
KEY_STATE_RELEASE_DEBOUNCE,//释放消抖状态
} KeyState;

// 按键事件类型
typedef enum {
KEY_EVENT_NONE,
KEY_EVENT_CLICK, // 单击
KEY_EVENT_DOUBLE_CLICK, //双击
KEY_EVENT_LONG_PRESS, // 长按
KEY_EVENT_RELEASE, // 弹起
KEY_EVENT_COMBO // 组合键
} KeyEventType;

typedef struct {
KeyState state; // 当前状态
uint32_t press_timestamp; // 按下时间戳
KeyEventType event; // 触发事件
uint8_t is_pressed; // 按下标志
} KeyHandle;

#define TIM_Period 20 // 按键扫描周期ms

#define DEBOUNCE_TIME 20/TIM_Period // 消抖时间ms
#define LONG_PRESS_TIME 1000/TIM_Period // 长按判定时间1000ms
#define DOUBLE_CLICK_TIMEOUT 300/TIM_Period // 双击按键判断时长ms
void keyInit(void);
void keyScan(void);
void keyAction(void);

#endif

Key.c代码:

#include "Key.h"
extern TIM_HandleTypeDef htim6;

KeyHandle Keys[16];//按键
/**
函数名:按键初始化
功能:
传参:
返回值类型:无
返回值意义:
*/
void keyInit()
{
for(int i=0;i<16;i++)
{
Keys[i].state=KEY_IDLE;
Keys[i].press_timestamp=0;
Keys[i].event=KEY_EVENT_NONE;
Keys[i].is_pressed=0;
}
}
/**
函数名:按键扫描状态机
功能:扫描按键状态切换
传参:按键键值
返回值类型:无
返回值意义:
*/
void Key_Scan_Single(uint8_t KeyNum)
{
uint8_t j =KeyNum%4;

switch (Keys[KeyNum].state){

case KEY_IDLE://空闲状态
Keys[KeyNum].press_timestamp=0;
Keys[KeyNum].is_pressed=0;//按下状态为 0
if(HAL_GPIO_ReadPin(KeyW1_GPIO_Port,KeyW1_Pin<<j)==GPIO_PIN_RESET){//低电平
Keys[KeyNum].state=KEY_STATE_PRESS_DEBOUNCE;
}
break;

case KEY_STATE_PRESS_DEBOUNCE://按下消抖状态
Keys[KeyNum].press_timestamp+=1;
if(HAL_GPIO_ReadPin(KeyW1_GPIO_Port,KeyW1_Pin<<j)==GPIO_PIN_SET){
Keys[KeyNum].state=KEY_IDLE; //认为是抖动
}
else if(Keys[KeyNum].press_timestamp>=DEBOUNCE_TIME ){
Keys[KeyNum].press_timestamp=0;
Keys[KeyNum].state=KEY_PRESSED;
}

break;

case KEY_PRESSED://按下状态
Keys[KeyNum].is_pressed=1;//按下状态为 1
if(HAL_GPIO_ReadPin(KeyW1_GPIO_Port,KeyW1_Pin<<j)==GPIO_PIN_RESET){//低电平
Keys[KeyNum].press_timestamp+=1;
}
else{ //读到高电平
Keys[KeyNum]. is_pressed=0;
Keys[KeyNum].state=KEY_DOUBLE_CHECK;
Keys[KeyNum].press_timestamp=0;
}
if(Keys[KeyNum].press_timestamp>LONG_PRESS_TIME){
Keys[KeyNum].state=KEY_LONG_PRESS;
}
break;

case KEY_DOUBLE_CHECK: //双击检测状态
if(HAL_GPIO_ReadPin(KeyW1_GPIO_Port,KeyW1_Pin<<j)==GPIO_PIN_SET){//读到高电平
Keys[KeyNum].press_timestamp+=1;
}else{//读到低电平
Keys[KeyNum].is_pressed=1;
Keys[KeyNum].state=KEY_DOUBLE_PRESS_DEBOUNCE;
Keys[KeyNum].press_timestamp=0;
}
if(Keys[KeyNum].press_timestamp>DOUBLE_CLICK_TIMEOUT){//超时,不再检测双击条件
Keys[KeyNum].event =KEY_EVENT_CLICK;//单击触发条件成立//触发单击事件
Keys[KeyNum].state=KEY_STATE_RELEASE_DEBOUNCE;
Keys[KeyNum].press_timestamp=0;
}
break;
case KEY_DOUBLE_PRESS_DEBOUNCE://按下消抖状态
Keys[KeyNum].press_timestamp+=1;
if(HAL_GPIO_ReadPin(KeyW1_GPIO_Port,KeyW1_Pin<<j)==GPIO_PIN_SET){
Keys[KeyNum].state=KEY_DOUBLE_CHECK; //认为是抖动,回到上一状态
Keys[KeyNum].press_timestamp=0;
}
else if(Keys[KeyNum].press_timestamp>=DEBOUNCE_TIME ){
Keys[KeyNum].press_timestamp=0;
Keys[KeyNum].state=KEY_DOUBLE_PRESS;
}
break;
case KEY_DOUBLE_PRESS://第二次按下确认状态
if(HAL_GPIO_ReadPin(KeyW1_GPIO_Port,KeyW1_Pin<<j)==GPIO_PIN_RESET){//L电平
Keys[KeyNum].is_pressed=1;
Keys[KeyNum].press_timestamp+=1;
}else{//读到H电平
Keys[KeyNum].is_pressed=0;
Keys[KeyNum].event = KEY_EVENT_DOUBLE_CLICK;//双击触发条件成立//触发双击事件
Keys[KeyNum].state = KEY_STATE_RELEASE_DEBOUNCE;
}
if(Keys[KeyNum].press_timestamp>DOUBLE_CLICK_TIMEOUT){//超时,不再检测双击条件
Keys[KeyNum].state=KEY_STATE_RELEASE_DEBOUNCE;
Keys[KeyNum].press_timestamp=0;
}
break;
case KEY_LONG_PRESS: //长按状态
Keys[KeyNum].event =KEY_EVENT_LONG_PRESS;//长按触发条件成立
if(HAL_GPIO_ReadPin(KeyW1_GPIO_Port,KeyW1_Pin<<j)==GPIO_PIN_SET){//高电平
Keys[KeyNum].is_pressed=0;
Keys[KeyNum].state=KEY_STATE_RELEASE_DEBOUNCE;
}
break;

case KEY_STATE_RELEASE_DEBOUNCE://释放消抖状态
Keys[KeyNum].press_timestamp+=1;
if(Keys[KeyNum].press_timestamp>DEBOUNCE_TIME &&(HAL_GPIO_ReadPin(KeyW1_GPIO_Port,KeyW1_Pin<<j)==GPIO_PIN_SET)){
Keys[KeyNum].press_timestamp=0;
Keys[KeyNum].state=KEY_IDLE;
Keys[KeyNum].event =KEY_EVENT_RELEASE;//触发释放事件
}
break;

default:break;
}
}
/**
函数名:按键组合检测
功能:按键组合检测
传参:
返回值类型:无
返回值意义:
*/
void Check_Combo(void) {
uint32_t current_time = HAL_GetTick();
// 组合按键检测
if (Keys[1].is_pressed && Keys[2].is_pressed ) {
Keys[1].event = KEY_EVENT_COMBO;
Keys[2].event = KEY_EVENT_COMBO;
}
}
/***
函数名:按键扫描
功能:
传参:
返回值类型:
返回值意义:
**/void keyScan()
{
uint8_t KeyNum;
uint8_t i,j;
for(i=0;i<4;i++){
HAL_GPIO_WritePin(KeyC1_GPIO_Port, KeyC1_Pin<<i, GPIO_PIN_RESET);
__NOP();
__NOP();
__NOP();//延时等待引脚电平稳定
for(j=0;j<4;j++){
KeyNum =j+4*i;//
Key_Scan_Single(KeyNum);//按键状态更新
Check_Combo();//组合按键检测
}
HAL_GPIO_WritePin(KeyC1_GPIO_Port, KeyC1_Pin<<i, GPIO_PIN_SET);
}
}

/***
函数名:按键执行程序
功能:根据键值执行代码
传参:Key_Enum 按键键值
返回值类型:
返回值意义:
**/
void keyAction()
{
switch (Keys[KET_7].event) {//按键7
case KEY_EVENT_NONE:
break;
case KEY_EVENT_CLICK:
Keys[KET_7].event = KEY_EVENT_NONE;
break;
case KEY_EVENT_DOUBLE_CLICK:
HAL_GPIO_WritePin(GPIOD,LED1_Pin,GPIO_PIN_RESET);
Keys[KET_7].event = KEY_EVENT_NONE;
break;

case KEY_EVENT_LONG_PRESS:
HAL_GPIO_WritePin(GPIOD,LED2_Pin,GPIO_PIN_RESET);
Keys[KET_7].event = KEY_EVENT_NONE;

break;
case KEY_EVENT_COMBO:
HAL_GPIO_WritePin(GPIOD,LED3_Pin,GPIO_PIN_RESET);
Keys[KET_7].event = KEY_EVENT_NONE;
break;
case KEY_EVENT_RELEASE:
Keys[KET_7].event = KEY_EVENT_NONE;
break;
}
switch (Keys[KET_8].event) {//按键8
case KEY_EVENT_NONE:
break;
case KEY_EVENT_CLICK:
Keys[KET_8].event = KEY_EVENT_NONE;
break;
case KEY_EVENT_DOUBLE_CLICK:
HAL_GPIO_WritePin(GPIOD,LED1_Pin,GPIO_PIN_RESET);
Keys[KET_8].event = KEY_EVENT_NONE;
break;

case KEY_EVENT_LONG_PRESS:
HAL_GPIO_WritePin(GPIOD,LED2_Pin,GPIO_PIN_RESET);
Keys[KET_8].event = KEY_EVENT_NONE;

break;
case KEY_EVENT_COMBO:
HAL_GPIO_WritePin(GPIOD,LED3_Pin,GPIO_PIN_RESET);
Keys[KET_8].event = KEY_EVENT_NONE;
break;
case KEY_EVENT_RELEASE:
Keys[KET_8].event = KEY_EVENT_NONE;
break;
}
switch (Keys[KET_9].event) {//按键9
case KEY_EVENT_NONE:
break;
case KEY_EVENT_CLICK:
HAL_GPIO_WritePin(GPIOD,LED1_Pin,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD,LED2_Pin,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD,LED3_Pin,GPIO_PIN_SET);

Keys[KET_9].event = KEY_EVENT_NONE;
break;
case KEY_EVENT_DOUBLE_CLICK:
HAL_GPIO_WritePin(GPIOD,LED1_Pin,GPIO_PIN_RESET);
Keys[KET_9].event = KEY_EVENT_NONE;
break;

case KEY_EVENT_LONG_PRESS:
HAL_GPIO_WritePin(GPIOD,LED2_Pin,GPIO_PIN_RESET);
Keys[KET_9].event = KEY_EVENT_NONE;

break;
case KEY_EVENT_COMBO:
HAL_GPIO_WritePin(GPIOD,LED3_Pin,GPIO_PIN_RESET);
Keys[KET_9].event = KEY_EVENT_NONE;
break;
case KEY_EVENT_RELEASE:
Keys[KET_9].event = KEY_EVENT_NONE;
break;
}
/**其余按键省略**/
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM6)
{
keyScan();
keyAction();
}
}

4.3状态机实现流水灯

第三章里实现流水灯的时间间隔采用的是HAL_Delay函数实现的,这种延时方式产生阻塞,耗费系统的资源。在实际开发程序代码要尽量避免阻塞的产生。利用状态机的思维,将流水灯程序加以改进,程序代码如下所示。

LEDBUF.h
#ifndef __LEDBUZ_H
#define __LEDBUZ_H
#include "main.h"

#define CODE_STEP 10
#define LED_Period 20
typedef struct {
GPIO_TypeDef *GPIOx; //GPIO指针
uint16_t GPIO_Pin;//引脚
GPIO_PinState PinStateOpen; //引脚状态
uint32_t Delay_ms;//延时时长
uint32_t count;//延时临时变量
}IO_Control;
typedef enum {
LED_IDLE,
LED_ON, //LED打开状态
LED_ON_DELAY,//LED延时状态
LED_OFF,//LED关闭状态
}LEDState;

void LED_Running(void);
void LED_Running2(void);

#endif

LEDBUF.c

#include "LEDBUZ.h"

IO_Control My_Gpio[CODE_STEP]={
{GPIOD, LED1_Pin,GPIO_PIN_RESET, 1000, 0},
{GPIOD, LED2_Pin,GPIO_PIN_RESET, 1000, 0},
{GPIOD, LED3_Pin,GPIO_PIN_RESET, 1000, 0},
{GPIOD, LED4_Pin,GPIO_PIN_RESET, 1000, 0},
{GPIOE, LED5_Pin,GPIO_PIN_RESET, 1000, 0},
{GPIOE, LED6_Pin,GPIO_PIN_RESET, 1000, 0},
{GPIOE, LED7_Pin,GPIO_PIN_RESET, 1000, 0},
{GPIOE, LED8_Pin,GPIO_PIN_RESET, 1000, 0},
{BUZZ_GPIO_Port, BUZZ_Pin,GPIO_PIN_SET, 100, 0},
{ERELAY_GPIO_Port, ERELAY_Pin,GPIO_PIN_SET, 900, 0},
};
LEDState LedState=LED_ON;
static uint8_t lednum = 0;
void LED_Running2()
{
switch(LedState)
{
case LED_ON:
HAL_GPIO_WritePin(My_Gpio[lednum].GPIOx,My_Gpio[lednum].GPIO_Pin,My_Gpio[lednum].PinStateOpen);//点亮
My_Gpio[lednum].count = My_Gpio[lednum].Delay_ms/LED_Period;
LedState = LED_ON_DELAY;
break;

case LED_ON_DELAY:
My_Gpio[lednum].count--;
if(My_Gpio[lednum].count<=0){
LedState = LED_OFF;
}
break;

case LED_OFF:
if(My_Gpio[lednum].PinStateOpen==GPIO_PIN_RESET)
HAL_GPIO_WritePin(My_Gpio[lednum].GPIOx,My_Gpio[lednum].GPIO_Pin,GPIO_PIN_SET);//
else HAL_GPIO_WritePin(My_Gpio[lednum].GPIOx,My_Gpio[lednum].GPIO_Pin,GPIO_PIN_RESET);//替换反转电平函数
My_Gpio[lednum].count = My_Gpio[lednum].Delay_ms/LED_Period;
LedState = LED_ON;
if(lednum<(CODE_STEP-1))lednum +=1;
else lednum = 0;
break;

case LED_IDLE://LED_IDLE时,相当于暂停流水灯
default:
break;
}
}

4.4状态机版按键控制流水灯

单击“OK”按键,流水灯停止流动;长按“OK”按键,流水灯继续流动,同时定义一个结构体LedStateSave用来暂存状态,确保每次流水灯都是从暂停位置重新启动,变量代码(有改动的部分)如下:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM6)
{
keyScan();
keyAction();
LED_Running2();
}
}

int main ()
{
keyInit();
/**省略其他**/
while (1)
{
//其他程序执行
}
}

extern LEDState LedState;
LEDState LedStateSave=LED_IDLE;//保存状态

keyAction()
{
/**省略其他**/
switch (Keys[KET_Ok].event) {
case KEY_EVENT_NONE:
break;

case KEY_EVENT_CLICK:
if(LedState!=LED_IDLE)LedStateSave=LedState;
LedState = LED_IDLE;//流水灯暂停
Keys[KET_Ok].event = KEY_EVENT_NONE;
break;

case KEY_EVENT_DOUBLE_CLICK:
Keys[KET_Ok].event = KEY_EVENT_NONE;
break;

case KEY_EVENT_LONG_PRESS:
if(LedStateSave!=LED_IDLE)LedState = LedStateSave;//流水灯启动
LedStateSave = LED_IDLE;
Keys[KET_Ok].event = KEY_EVENT_NONE;

break;
case KEY_EVENT_COMBO:
Keys[KET_Ok].event = KEY_EVENT_NONE;
break;
case KEY_EVENT_RELEASE:
Keys[KET_Ok].event = KEY_EVENT_NONE;
break;
}
}

按键的整个程序架构包含了按键触发后的所有事件处理,对于应用来说只用到了单击和长按。

单击按键事件触发后,判断流水灯是否是处于“空闲”状态,保存当前状态值的同时将流水灯状态设置为暂停。

长按按键事件触发后,判断流水灯是否是处于“运行”状态,保存当前状态值的同时将流水灯状态设置为运行。

4.5课后练习

使用其他按键实现花样流水灯。